﻿#!/usr/bin/env python
#-*- coding:utf-8 -*-
# ZheoFX~ 2009 - 2010
# @Zheo @Nande! @Alquimista

# Modulos de python
import itertools
import math
import random
import codecs
import re
import os
import sys
import time

# Modulos externos
try:
    import cairo
except ImportError:
    print 'You need to instal pycairo'
if sys.platform == "win32":
    try:
        import win32gui as wgui
        import win32con as wcon
    except ImportError:
        print 'You need to install pywin32'
else:
    try:
#        import wx
#        myapp = wx.App(0)
        from PyQt4 import QtGui
        app = QtGui.QApplication([])
    except ImportError:
        print 'You need to install wxpython'
#        print 'You need to install pyqt'

# Modulos internos
from libs.cache import Mem


VERSION = (0, 1, 0, 'beta', 45)
DLG = []
ESTILOS = []
FORMATO = []

# Regex patterns
# Mejorados y comentados
TAGSFX = re.compile(
    r'''
    ({[\s\w\d\\-]+})*           # remove all the tags
                                # {comment}{\be1}{\k86}fai{\k20}ry --> fairy
    ''',
    re.IGNORECASE | re.UNICODE | re.VERBOSE)

KARA = re.compile(
    r'''
    (?<=[\\k|\\ko|\\kf])(\d+)    # k duration in centiseconds(k, K, kf, ko)
    (?:\\[\w\d]+)*               # ignore tags before inlinefx
    (?:\\-)*([\w\d]+)*           # inlinefx
    (?:\\[\w\d]+)*               # ignore tags after inlinefx
    }                            # start of text
    (\s+)*                       # prespace
    ([^\s\{\}]*)                 # stripped text
    (\s+)*                       # postspace
    ''',
    re.IGNORECASE | re.UNICODE | re.VERBOSE)


###############################################################################
######################### *- Clases Principales -* ############################
###############################################################################
# Class Sil
class Sil(object):
    def __init__(self):
        self.i = 0
        self.text = ''
        self.start = 0
        self.dur = 0
        self.mid = 0
        self.end = 0
        self.kdur = 0
        self.inline_fx = ''
        self.width = 0
        self.height = 0
        self.left = 0
        self.center = 0
        self.right = 0
        self.prespace = 0
        self.postspace = 0
        self.stripped = ''


# Class Char
class Chars(object):
    def __init__(self):
        self.i = 0
        self.text = ''
        self.start = 0
        self.dur = 0
        self.syldur = 0
        self.end = 0
        self.sylstart = 0
        self.sylend = 0
        self.width = 0
        self.height = 0
        self.left = 0
        self.center = 0
        self.right = 0


# Class Lineas
class Line(object):
    def __init__(self):
        self.i = 0
        self.text = ''
        self.start = '0:00:00.00'
        self.dur = 1000
        self.end = '0:00:01.00'
        self.layer = 0
        self.style = 'Default'
        self.styleref = Style()
        self.actor = ''
        self.marginl = self.marginr = self.marginv = '0000'
        self.effect = ''
        self.kara = 'ZheoFX'
        self.kara_n = 0
        self.charas = ''
        self.charas_n = 0
        self.x = 0
        self.y = 0
        self.width = 0
        self.height = 0
        self.left = 0
        self.center = 0
        self.right = 0
        self.top = 0
        self.middle = 0
        self.bottom = 0
        self.darx = 0
        self.dary = 0


# Class Estilos
class Style(object):
    def __init__(self):
        self.name = 'Default'
        self.font_name = 'Arial'
        self.font_size = 30
        self.c1 = '&H00FFFFFF'
        self.c2 = '&H00FFFF00'
        self.c3 = '&H00FF0000'
        self.c4 = '&H00000000'
        self.bold = '-1'
        self.italic = '0'
        self.underline = '0'
        self.strike = '0'
        self.scale_x = 100
        self.scale_y = 100
        self.space = 0.0
        self.angle = 0
        self.bord = 2
        self.shad = 0
        self.align = 8
        self.ml = 15
        self.mr = 0
        self.mv = 15
        self.encoding = 1


class AssWritter(object):
    def __init__(self):
        self.ini = []

    def add_section(self, section):
        self.ini.append("[%s]" % section)

    def add_newline(self):
        self.ini.append('')

    def add_key(self, key, value):
        self.ini.append("%s: %s" % (key, value))

    def add_comment(self, comment):
        self.ini.append("; %s" % comment)

    def add_keylist(self, key, alist):
        for element in alist:
            self.ini.append('%s: %s'% (key, element))

    def show(self):
        return "\n".join(self.ini)


###############################################################################
####################### *- Funciones principales -* ###########################
###############################################################################
# Funcion para las llaves
@Mem
def fx(txt, tipo=None, kdur=None):
    """Retorna {..}"""
    if tipo:
        return '{\\%s%d}{%s}' % (tipo, kdur, txt)
    else:
        return '{%s}' % txt


# Funciones de tiempo
@Mem
def Seg2Ms(valor):
    """Retorna Segundos a Ms"""
    return valor * 1000


@Mem
def Frame2Ms(frames, fps):
    """Retorna Frame a Ms"""
    return round(Seg2Ms(frames / float(fps)), 2)


@Mem
def Ms2Frame(ms, fps):
    """Retorna Ms a Frame"""
    seg = ms / 1000.0
    return int(round(fps * seg, 0)) # fps x seg = frames

@Mem
def t_ms(tiempo):
    """Retorna tiempo Hora a forma de MS"""
    # idea: @kafx (thanks Jero)
    h, m, s = tiempo.split(':')
    s, cs = s.split('.')
    return Seg2Ms(int(h) * 3600 + int(m) * 60 + int(s)) + int(cs) * 10


@Mem
def t_hora(t):
    """Retorna tiempo MS a forma de Hora"""
    seg, ms = divmod(t, 1000)
    m, s = divmod(seg, 60)
    h, m = divmod(m, 60)
    cs = ms / 10.0
    return '%01d:%02d:%02d.%02d' % (h, m, s, cs)

# Funcion Text Extents Win32 y wx
@Mem
def Text_Extents(txt, line, ajuste=(1 / 1.2)):
    """Retorna Ancho y Alto de un texto"""
    # idea: http://devel.aegisub.org/browser/trunk/aegisub/src/auto4_base.cpp
    # Funciona en windows
    fontsize = line.styleref.font_size * 64
    spacing = line.styleref.space * 64

    if sys.platform == "win32":  # para win
        thedc = wgui.CreateCompatibleDC(0) # necesarios para win

        lf = wgui.LOGFONT()
        lf.lfHeight = fontsize
        lf.lfWeight = (line.styleref.bold == "-1") and wcon.FW_BOLD or wcon.FW_NORMAL
        lf.lfItalic = 0
        lf.lfUnderline = 0
        lf.lfStrikeOut = 0
        lf.lfCharSet = line.styleref.encoding
        lf.lfOutPrecision = wcon.OUT_TT_PRECIS
        lf.lfClipPrecision = wcon.CLIP_DEFAULT_PRECIS
        lf.lfQuality = wcon.ANTIALIASED_QUALITY
        lf.lfPitchAndFamily = wcon.DEFAULT_PITCH
        lf.lfFaceName = line.styleref.font_name

        thefont = wgui.CreateFontIndirect(lf)
        wgui.SetMapMode(thedc, 4)
        wgui.SelectObject(thedc, thefont)

        if spacing != 0:
            width = 0
            for char in txt:
                te = wgui.GetTextExtentPoint32(thedc, txt)
                width += te[0] + spacing
        else:
            te = wgui.GetTextExtentPoint32(thedc, txt)
            width = te[0]
        wgui.DeleteObject(thedc)
        wgui.DeleteObject(thefont)

    else: # no windows (y funciona en linux)
        fontsize *= ajuste
        font_name = line.styleref.font_name

        scalex = line.styleref.scale_x
        scaley = line.styleref.scale_y

        italic = line.styleref.italic
        bold = line.styleref.bold

        font = QtGui.QFont(font_name)
        font.setPixelSize(fontsize)
        font.setItalic(bool(eval(italic)))
        font.setBold(bool(eval(bold)))
        font.setLetterSpacing(QtGui.QFont.AbsoluteSpacing, spacing)

        fm = QtGui.QFontMetricsF(font)

        w = 0
        if spacing != 0:
            for i, s in enumerate(txt):
                w += fm.charWidth(txt, i) + spacing
            pixelsWide = w
        else:
            pixelsWide = fm.width(txt)

        pixelsHigh = line.styleref.font_size

        return ((pixelsWide / 64.0) * (scalex / 100.0),
                (pixelsHigh / 64.0) * (scaley / 100.0))


def Line_PosXY(resx, resy, line):
    # Funcion Posiciones de la Linea
    """Retorna x-y de una linea"""
    lx = ly = lleft = lcenter = lright = ltop = lmid = lbot = 0
    align = line.styleref.align

    mb = line.styleref.mv - 0.5  # margen bottom
    mt = line.styleref.mv + 0.5  # margen top
    ml = line.styleref.ml + 0.5  # margen left
    mr = line.styleref.mr - 0.5  # margen right

    # line x
    if align == 1 or align == 4 or align == 7:  # izq
        lleft = ml
        lcenter = lleft + (line.width / 2.0)
        lright = lleft + line.width
        lx = lleft + 0.5
    elif align == 2 or align == 5 or align == 8:  # centro
        lcenter = resx / 2.0
        lleft = lcenter - (line.width / 2.0)
        lright = lcenter + (line.width / 2.0)
        lx = lleft
    elif align == 3 or align == 6 or align == 9:  # der
        lleft = resx - line.styleref.mr - line.width
        lcenter = lleft + (line.width / 2.0)
        lright = lleft + line.width
        lx = lleft + 0.5

    # line y
    if align == 7 or align == 8 or align == 9:  # arriba
        ltop = mt
        lmid = ltop + (line.height / 2.0)
        lbot = ltop + line.height
        ly = lmid + 1

    elif align == 4 or align == 5 or align == 6:  # centro
        lmid = resy / 2.0
        ltop = lmid - (line.height / 2.0)
        lbot = lmid + (line.height / 2.0)
        ly = lmid
    elif align == 1 or align == 2 or align == 3:  # abajo
        lbot = resy - mb
        lmid = lbot - (line.height / 2.0)
        ltop = lbot - line.height
        ly = lmid
    return lx, ly, lleft, lcenter, lright, ltop, lmid, lbot


# Funcion para generar el dialog
@Mem
def GenDialog(layer, start, end, style, fx, texto=None):
    """Generador de Dialogos"""
    if texto is None:
        texto = 'ZheoFx'
    if fx is None:
        fx = be()
    DLG.append(
    '%s,%s,%s,%s,%s,0000,0000,0000,%s,%s%s' % \
    (layer, t_hora(start), t_hora(end), style, '', '', fx, texto))


# Funcion que valida resolucion segun Vobsub
@Mem
def ValidRes(resx, resy):
    if resx == 0 and resy == 0:
        resx, resy = 384, 288
    elif resx == 0:
        if resy == 1024:
            resx = 1280
        else:
            resx = resy / 3 * 4
    elif resy == 0:
        if resx == 1280:
            resy = 1024
        else:
            resy = resx * 3 / 4
    return resx, resy


@Mem
# Funcion que valida int y float
def ValidType(num, redondeo=2):
    """Retorna un numero redondeado si es flotante como una cadena"""
    return '%g' % round(num, redondeo)


@Mem
# Funcion que valida Hexadecimales
def ValidHex(valor):
    """Valida que los valores se encuentren en el rango de 0 a 255"""
    if type(valor) == str:
        raise TypeError('\nValidHex acepta solo numeros')
    else:
        #clamp
        return max(0, min(valor, 255))

@Mem
# Funcion que valida Cantidad de digitos en el Color
def ValidAssColor(valor):
    """Verifica si tiene 6 digitos el Valor, coloca &H_color_&"""
    if len(valor) <= 6:
        return '&H%s&' % valor.upper()
    else:
        return valor.upper()


# Funcion que retorna un mensaje de error
@Mem
def ValidError(msj):
    """Imprime mensaje de Error"""
    print raw_input(msj)


###############################################################################
######################## *- Funcion Kara y Char -* ############################
###############################################################################
def GetKar(texto, line, resx, ajuste=(1 / 1.2)):
    """Retorna Silabas, Chars y Linea Texto"""
    syls, letras = [], []
    txts, tiempo = "", line.start
    curx, linew = 0.0, 0.0

    for match in re.finditer(KARA, texto):

        ti, ifx, prs, tx, ps = match.groups()
        ifx = ifx if ifx else ''
        prs = prs if prs else ''
        tx = tx if tx else ''
        ps = ps if ps else ''

        syl = Sil()
        syl.dur = int(ti) * 10
        syl.mid = syl.dur / 2.0
        syl.kdur = int(ti)
        syl.start = tiempo
        syl.end = tiempo + syl.dur
        tiempo = syl.end
        syl.inline_fx = ifx
        syl.text = tx
        syl.stripped = prs + tx + ps.strip()

        syl.width, syl.height = Text_Extents(syl.text, line, ajuste)
        syl.prespace = Text_Extents(prs, line, ajuste)[0]
        syl.postspace = Text_Extents(ps, line, ajuste)[0]

        syl.left = curx + syl.prespace
        syl.center = syl.left + (syl.width / 2.0)
        syl.right = syl.left + syl.width
        curx += syl.prespace + syl.width + syl.postspace
        syls.append(syl)  # silabas

        charstart = syl.start
        if tx != '':
            charsdur = syl.dur / float(len(tx))
        left = syl.left
        for ctx in syl.text:
            chars = Chars()
            chars.start = charstart
            chars.end = charstart + charsdur
            chars.dur = charsdur
            chars.syldur = syl.dur
            chars.sylstart = syl.start
            chars.sylend = syl.end
            charstart = chars.end
            chars.text = ctx
            chars.width, chars.height = Text_Extents(chars.text, line, ajuste)

            chars.left = left + 1
            chars.center = left + (chars.width / 2.0)
            chars.right = left + chars.width
            left+= chars.width
            letras.append(chars)  # chars
        txts += prs + tx + ps
        linew += syl.prespace + syl.width + syl.postspace

    linew += 6
    if txts == '':
        # este texto es el de linea usado en la traduccion
        txts = re.sub(TAGSFX,'',texto) # lineas (quitando las tags)
        linew = Text_Extents(txts, line, ajuste)[0]

    # Retorna (syls, chars, linea y ancho linea)
    return syls, letras, txts, linew


###############################################################################
####################### *- Funciones de Archivos -* ###########################
###############################################################################
# Abre el archivo y lo parsea
def ParAss(archivo, ajuste=(1 / 1.2)):
    """Pasea Archivo .ass"""
    LINEAS = []
    STYLES = {}
    if not archivo.lower().endswith('.ass'): # verificamos si es .ass
        ValidError("No es archivo .ass v4.00+\
                   \nThis file is not .ass v4.00+\
                   \n\n>>Presiona cualquier tecla * Press any key<<")
    archivo_ass = codecs.open(archivo, 'r', 'utf-8-sig')
    for l in archivo_ass:
        if not l.startswith((';', '[', ' ')):
            line = Line()
            styles = Style()
            syl = Sil()
            try:
                name, datos = l.split(':', 1)
            except Exception:
                continue
            tipo = name.lower()
            if tipo == 'playresx':
                RESX = int(datos)
            elif tipo == 'playresy':
                RESY = int(datos)
            elif tipo == 'format':
                FORMATO.append(datos)
            elif tipo == 'style':
                (name, fn, fs, color1, color2, color3, color4, b, i, u, s, fscx,
                 fscy, fsp, frz, opaque, bord, shad, an, margin_l, margin_r,
                 margin_v, fe) = datos.strip().split(',', 22)
                styles.name = name
                styles.font_name = fn
                styles.font_size = int(fs) #arroja error si no se deja int
                styles.c1 = color1[4:]
                styles.c2 = color2[4:]
                styles.c3 = color3[4:]
                styles.c4 = color4[4:]
                styles.bold = b #son boolean, asi k mejor dejarlos sin int
                styles.italic = i
                styles.underline = u
                styles.strike = s
                styles.scale_x = float(fscx)
                styles.scale_y = float(fscy)
                styles.space = float(fsp)
                styles.angle = int(frz)
                styles.bord = float(bord)
                styles.shad = float(shad)
                styles.align = int(an)
                styles.ml = int(margin_l)
                styles.mr = int(margin_r)
                styles.mv = int(margin_v)
                styles.encoding = int(fe)
                ESTILOS.append(datos)
                STYLES[styles.name] = styles
            elif tipo == 'dialogue':
                (layer, start_t, end_t, stylo, actor, margin_l, margin_r,
                 margin_v, effect, text) = datos.strip('\n').split(',', 9)
                line.layer = int(layer)
                line.start = t_ms(start_t)
                line.end = t_ms(end_t)
                line.dur = line.end - line.start
                line.style = stylo
                try:
                    line.styleref = STYLES[line.style]
                except KeyError:
                    ValidError("* No se han encontrado Estilos en tu archivo .ass! *\
                               \n* Can't find any styles in your .ass File! *\
                               \n\n>>Presiona cualquier tecla * Press any key<<")
                    break
                line.actor = actor
                line.marginl = margin_l
                line.marginr = margin_r
                line.marginv = margin_v
                line.effect = effect
                getkara = GetKar(text, line, RESX, ajuste)
                line.kara, line.charas, line.text, line.width = getkara
                line.height = line.styleref.font_size
                # para su uso: (valor * line.darx)
                line.darx = line.styleref.scale_x / 100.0
                # para su uso: (valor * line.dary)
                line.dary = line.styleref.scale_y / 100.0
                # line kara sin contar las silabas vacias
                line.kara_n = 0
                for i in line.kara:
                    if i.text not in ('', ' '):
                        line.kara_n += 1
#                line.kara_n = len(line.kara)
                getlinepos = Line_PosXY(RESX, RESY, line)
                (line.x, line.y, line.left, line.center, line.right, line.top,
                 line.middle, line.bottom) = getlinepos
                #line.center_tra, line.center_tra, line.right_tra
                line.chara_n = len(line.charas)
                LINEAS.append(line)
    ## Retorna (Resx, Resy, Estilos, [Karas y Tiempos])
    return ValidRes(RESX, RESY), STYLES, LINEAS


def Save(name, file_out, RESX, RESY):
    """Graba el archivo final"""

    FESTILO = 'Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour,\
BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle,\
BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding'
    FEVENTO = 'Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text'
    EDEFAULT = 'Default,Arial,20,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2,0,2,10,10,10,1'

    ass_w = AssWritter() # Va guardando lo necesario para generar el ass.
    ass_w.add_section('Script Info')
    ass_w.add_comment('Script Generado por ZheoFX')
    ass_w.add_comment(str(VERSION))
    ass_w.add_comment('>>> Info: http://my.opera.com/zheo23 <<<')
    ass_w.add_comment('>>> web: http://zheofx.assembla.me/ <<<\n')
    ass_w.add_key('Title', name)
    ass_w.add_key('ScriptType', u'v4.00+')
    ass_w.add_key('PlayResX', str(RESX))
    ass_w.add_key('PlayResY', str(RESY))
    ass_w.add_newline()
    ass_w.add_section('V4+ Styles')
    ass_w.add_key('Format', FESTILO)
    if ESTILOS:
        ass_w.add_keylist('Style', ESTILOS)
    else:
        ass_w.add_key('Style', EDEFAULT)
    ass_w.add_newline()
    ass_w.add_section('Events')
    ass_w.add_key('Format', FEVENTO)
    ass_w.add_keylist('Dialogue', DLG)

    final_fx = ass_w.show()
    final = codecs.open(file_out, 'w', 'utf-8-sig')
    final.write(final_fx)
    final.close

def progressbar(it, prefix = "Escribiendo", prog="#",
                sufix="Lineas", size = 50):
    """Barra de Progreso"""
    count = len(it)
    def _show(_i):
        x = int(size * _i / count + 2)
        sys.stdout.write("%s[%s%s] %i/%i %s\r" % (prefix, prog * x,
                                                  "." * (size - x),
                                                  _i + 1, count, sufix))
        sys.stdout.flush()
    _show(0)
    for i, item in itertools.izip(itertools.count(), it):
        yield item
        _show(i + 1)
    sys.stdout.write("\n")
    sys.stdout.flush()


def LoadStart():
    """Carga algunas variables iniciales"""
    tini = time.clock() if sys.platform == "win32" else time.time()
    syli, tempo = 0, 0
    print "ZheoFx" , VERSION, "\n"
    return tini, syli, tempo


def print_output(tini):
    """Imprime el tiempo total del generado del .ass final"""
    if sys.platform == "win32":
        tfin = time.clock() - tini
    else:
        tfin = time.time() - tini
    tfin_print = tfin * 1000
    unidad = "ms"
    if tfin_print >= 1000:
        tfin_print = tfin
        unidad = "s"
    if tfin >= 60:
        tfin_print = tfin / 60.0
        unidad = "m"
    print "\nScript Generado en: %s %s" % (ValidType(tfin_print), unidad)
